Версия для печати<% ASP на блюдечке %>. Часть 17

Иерархическая навигационная система меню (с помощью ASP, XML и JavaScript)

Рубен Садоян (rouben@iname.com)

Введение

Историческая справка

XML — хранилище данных иерархии меню

Итак…

Отображение иерархии — файл Menu.asp

Рекурсивное построение меню — функция DisplayNode()

Вспомогательные JavaScript-функции

Правый фрейм — файл Content.asp

Заключение

Введение

Как известно, одной из наиболее важных составляющих любого приложения является система навигации в содержании. Это та неотъемлемая составляющая, благодаря которой пользователи получают удобный и быстрый доступ к нужному разделу информации. Как показал опыт развития интеллектуальных интерфейсов современных операционных систем, наибольшей интуитивностью обладает иерархический древовидный интерфейс — такой, в частности, служит основой навигации в различных Windows-приложениях (Windows Explorer, Microsoft Management Console, Registry Editor и т.д.). Информация отображается иерархически, причем дочерние разделы отображаются правее (глубже) разделов родителей. Подобная система уже давно зарекомендовала себя как одна из самых наглядных, например когда дело касается организации систем отображения иерархий.

В начало В начало

Историческая справка

До недавнего времени иерархическая навигационная система была присуща лишь так называемым настольным (Desktop) приложениям, или, проще говоря,  приложениям, выполняющимся в операционной системе. Позднее стали появляться системы, эмулирующие поведение своих старших программных «собратьев», однако основным их недостатком являлась огромная транзакционная нагрузка на сервер. Дело в том, что для отображения различных состояний дерева использовались различные html-файлы и, таким образом, задача сводилась к передаче управления от одного html-файла другому, а это при увеличении количества уровней в иерархии приводило к множеству проблем как в ходе разработки, так и при использовании таких систем. Затем появились аплеты Java, и хотя они решали задачу отображения информации требуемым образом, однако выполнялись интерпретатором Java не на сервере, а непосредственно в браузере клиента, что создавало дополнительный ненужный трафик. И вот, наконец, в последнее время, благодаря развитию технологий Internet-программирования и появлению ASP-технологии, такой способ организации представления данных стал в полной мере доступен и Web-приложениям.

Многие наши читатели вправе усомниться в необходимости иерархического дерева. И будут правы, если посчитают подобную систему навигации излишней роскошью на сайте с относительно небольшим количеством страниц. Но везде, где структура отображаемых данных представлена иерархией и количество отдельных страниц данных велико, она окажется просто незаменимой.

В начало В начало

XML — хранилище данных иерархии меню

Здесь может возникнуть вопрос: а при чем тут XML? Отвечаем: лучше всего разработать систему таким образом, чтобы предоставить возможность в любой момент изменить как структуру, так и наименования иерархического меню, сделав сам код по его отображению независимым от структуры. Конечно, можно было бы создать несколько таблиц в какой-нибудь реляционной СУБД и, последовательно подсоединив их одну к другой, заполнить связанными иерархией отношений значениями. Однако СУБД — не самый простой и удобный способ решения этой задачи. При увеличении уровней вложенности как нельзя лучше подходит XML-организация хранения иерархии. Для удобства изложения материала и большей наглядности представим меню как информацию по отдельной стране, например по некоторым фактам из истории США:

<country type="root" value="United States of America" url="content.asp?page=usa">
  


 <states type="folder" value="States" url="content.asp?page=states">
      <state type="document" url="content.asp?page=ca" value="California"/>
      <state type="document" url="content.asp?page=nj" value="New Jersey"/>
     <state type="document" url="content.asp?page=az" value="Arizona"/>
  </states>
 


  <hist_fig type="folder" value="Historical Figures" url="content.asp?page=histfig">
     <figure type="document" value="George Washington" url="content.asp?page=george"/>
     <figure type="document" value="Thomas Jefferson" url="content.asp?page=tom"/>
  </hist_fig>
 


  <history type="folder" value="History" url="content.asp?page=history">
    <Cent20 type="folder" url="content.asp?page=20th" value="20th Century">
       <inventions type="folder" url="content.asp?page=inv" value="Inventions">
         <technologies type="folder" url="content.asp?page=tec" value="Technology">
           <radio type="folder" url="content.asp?page=radio" value="Radio">
             <bground type="document" url="content.asp?page=invprof" value="Inventor Profile"/>
               <bground type="document" url="content.asp?page=first" value="First Use"/>
          </radio>
          <computers type="folder" url="content.asp?page=computers" value="Computers">
             <begin type="folder" url="content.asp?page=begin" value="Beginnings">
               <summary type="document" url="content.asp?page=sum" value="Summary"/>
               <transistor type="folder" url="content.asp?page=trans" value="Transistor">
                   <trans type="document" url="content.asp?page=inventor" value="Inventor"/>
                   <trans type="document" url="content.asp?page=app" value="Applications"/>
                </transistor>
              </begin>
         </computers>
      </technologies>
    </inventions>
    <wars type="folder" url="content.asp?page=wars" value="Wars">
         <war type="document" url="content.asp?page=wwi" value="World War I"/>
         <war type="document" url="content.asp?page=wwii" value="World War II"/>
         <war type="document" url="content.asp?page=viet" value="Vietnam"/>
    </wars>
</Cent20>
<Cent21 type="folder" url="content.asp?page=21st" value="21st Century"/>
</history>
</country>

Как видите, XML во многом напоминает HTML, однако, в отличие от последнего, XML не ограничивает разработчика в определении тэгов и организации структуры хранения данных. В вышеприведенном примере все тэги содержат пункты меню и имеют по три атрибута:

В начало В начало

Итак…

Представим себе наш интерфейс в виде двух вертикальных фреймов: левого, служащего для отображения иерархии объектов меню, и правого — для отображения содержимого текущего пункта меню. Левый фрейм представим файлом menu.asp, а правый файлом content.asp (см. рис. 1):

<html>
<head>
<title><% ASP на блюдечке %>. Часть 17</title>
</head>
 


<FRAMESET cols="250,*"> 
  <FRAME src="menu.asp" name="treeframe" > 
  <FRAME SRC="content.asp" name="basefrm"> 
</FRAMESET> 
</HTML>
В начало В начало

Отображение иерархии — файл Menu.asp

Для начала определим табличку стиля node, который будем использовать в дальнейшем:

<STYLE TYPE="text/css">
<!--
   .node { color: black;
      font-family : "Helvetica", "Arial", "MS Sans Serif", sans-serif;
      font-size : 9pt;}
   .node A:link { color: black; text-decoration: none; }
   .node A:visited { color: black; text-decoration: none; }
   .node A:active { color: black; text-decoration: none; }
   .node A:hover { color: black; text-decoration: none; }

-->
</STYLE>

Далее создадим экземпляр ActiveX объекта и загрузим в него XML-файл с иерархией нашего меню:

<%

 On Error Resume Next

 dim sXMLSourceFile

 dim iTotal, sLeftIndent, bLoaded
               

 iTotal = 0

 sLeftIndent = ""

 sXMLSourceFile = "menuitems.xml"
 


 'Создаем экземпляр COM объекта XML

 Set objDocument = Server.CreateObject("MSXML2.FreeThreadedDOMDocument.3.0")

 if objDocument is nothing then
   Response.Write "objDocument object not created<br>"

 else

 If Err Then 
  Response.Write "XML DomDocument Object Creation Error - <BR>"
  Response.write Err.Description

 else
  objDocument.async = false
  bLoaded = objDocument.load(Server.MapPath(sXMLSourceFile))
  if (bLoaded = False) then
   sbShowXMLParseError objDocument 
 else
  dim arArray(3)
  arArray(0) = objDocument.firstChild.getAttribute("value") 
  'Корневой уровень в нашей иерархии
  arArray(1) = "History"
  'Строим таблицу нашего меню

 %>
  <table border="0" cellspacing="0" cellpadding="0" width="100%">
  <tr><td>
  <%
    'Покажем текущий пункт нашего иерархического меню
    DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray
  %>
  </td></tr>
  </table>
<%
 end if
 end if
 end if
%>

Как видите, вызов процедуры

DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray 

собственно говоря, служит для создания иерархии нашего меню. Параметр iTotal, передающийся по ссылке, отслеживает общее количество элементов нашего меню и будет использоваться в дальнейшем. Функция продолжает рекурсивно вызывать саму себя, пока не будет осуществлен обход всего дерева элементов меню. Так, параметр iTotal используется для определения массивов, служащих для управления отображением нашего меню:

var arClickedElementID = 
       new Array(<% for i = 1 to iTotal %> "<%=i%>"<%if
 i < iTotal then%>,<%end if%> <%next%>);
 
 
var arAffectedMenuItemID = 
      new Array(<% for i = 1 to iTotal %> "<%=i+1%>"<%if
 i < iTotal then%>,<%end if%> <%next%>); 

Теперь HTML-страница сформирована, и на этом этапе XML-файл совершил свою функцию: данные из него прочитаны и дерево уже построено. Но по-прежнему «черным ящиком» остается функция DisplayNode().

В начало В начало

Рекурсивное построение меню — функция DisplayNode()

Ето, по сути, и есть ядро нашей системы, осуществляющее обход дерева и формирующее HTML-код. У процедуры четыре входных параметра: objNodes, iElement, sLeftIndent и arOpenFolders. Первый — objNodes — служит для определения всего набора уровней иерархии, начиная с уровня root. Второй —  iElement — содержит целое идентифицирующее количество уже отображенных элементов иерархии. Этот параметр передается по ссылке и таким образом обновляется при каждом вызове процедуры. Параметр sLeftIndent передает строку, содержащую HTML-форматирование для отображения того или иного элемента меню. Параметр arOpenFolders — это массив наших элементов.

Кроме того, в ходе каждого выполнения процедуры DisplayNode() проверяется:

 For Each oNode In objNodes 
   bHasChildren = oNode.hasChildNodes 
   if not(oNode.nextSibling is nothing) then 
     bIsLast = false 
   else 
     bIsLast = true 
   end if 

   if oNode.nodeType = NODE_ELEMENT Then 
      sAttrValue = oNode.getAttribute("value") 
      sNodeType = lcase(oNode.getAttribute("type")) 
      bForceOpen = fnInArray(sAttrValue, arOpenFolders)
      sURL = oNode.getAttribute("url") 
      if (sNodeType = "document") then 
  %> 
  <table border="0" cellspacing="0" cellpadding="0" width="100%"> 
  <tr valign="bottom"> 
  <% Response.write sLeftIndent %> 
  <td width="31"><img src="images/<%=fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen)%>" 
    width="31" height="16" border="0"></td> 
  <td nowrap class="node">&nbsp;<a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td> 
  </tr> 
  </table> 
  <% else %> 
  <table border="0" cellspacing="0" cellpadding="0" width="100%"> 
  <tr valign="bottom"> 
  <% Response.write sLeftIndent %> 
  <td width="31"><img class="LEVEL<%=iElement%>" src="images/ 
  <%= fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen) %>" id="<%=iElement%>" 
   width="31" height="16" border="0"></td> 
  <td nowrap class="node">&nbsp;<a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td> 
 </tr> 
 </table>

<% 
  If bHasChildren Then
   iElement = iElement + 1
%>

<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr valign="bottom" class="LEVEL<%=iElement%>" id="<%=iElement%>" style="display: 

<%
 if (fnInArray(sAttrValue, arOpenFolders) = false) then%>none<%end if
%>
 
">
 <td>
 <%
  sTempLeft = sLeftIndent
  if (iElement > 1) then
    sLeftIndent = fnBuildLeftIndent(oNode, bIsLast, sLeftIndent) 
 end if 
  
 'Рекурсивный вызов и продолжение обхода дерева вглубь. 
 DisplayNode oNode.childNodes, iElement, sLeftIndent, arOpenFolders
  
 sLeftIndent = sTempLeft 
 %>      
 </td> 
 </tr> 
 </table> 
 <% 
  End If 
 end if 
 End If 
Next

Как видите, нам осталось разобраться в нескольких вспомогательных JavaScript-функциях, служащих для выбора необходимого графического значка (fnChooseIcon), функции обхода массива при поисках нужного значения (fnInArray), отрисовки элементов (fnBuildLeftIndent) и показа сообщения об ошибке с указанием строки, колонки и другой полезной отладочной  информации (sbShowXMLParseError).

В начало В начало

Вспомогательные JavaScript-функции

Прежде всего нам необходимо понять ту роль, которую играют два массива данных:

var arClickedElementID = new Array( "1", "2", "3", "4", "5", "6", ...);
var arAffectedMenuItemID = new Array( "2", "3", "4", "5", "6", ...);

Эти массивы служат для определения отношения «родитель-потомок», показывая, какие элементы нашего списка должны быть свернуты, а какие развернуты. Первый массив (arClickedElementID[]) содержит идентификаторы всех элементов нашей иерархии. Второй (arAffectedMenuItemID[]) — идентификаторы всех потомков заданного элемента из первого массива. В приведенном выше примере это — потомки первого элемента первого массива данных.

Развертывание/свертывание элементов — функция doChangeTree()

Сначала определим функцию-реакцию на действия пользователя. Перехватим событие onClick нашего HTML-документа:

document.onclick = doChangeTree;

Первое, что нам надо будет сделать, как только пользователь нажмет на тот или иной пункт в иерархии, это получить ссылку на «нажатый» элемент. Далее продолжаем только в том случае, если элемент представляет собой класс и если в начале его имени содержится строковая константа "LEVEL":

 srcElement = window.event.srcElement;

 if(srcElement.className.substr(0,5) == "LEVEL") 
 {

Затем мы должны сослаться на потомок данного родителя, который должен быть развернут или свернут:

targetElement = fnLookupElementRef(srcElement.id)

Для этого мы передаем параметр ID нажатого пользователем элемента меню функции fnLookupElementRef(), которая с помощью описанных нами выше массивов arClickedElementID[] и arAffectedMenuItemID[] получает ссылку на требуемый потомок, как показано ниже:

for (i=0; i<arClickedElementID.length; i++)
  if (arClickedElementID[i] == sID)
   return document.all(arAffectedMenuItemID[i]);

Нам потребуется также исключить обработки нажатий на пустых папках. Для этого заранее проименуем соответствующие файлы с изображениями пустых папок таким образом, чтобы они содержали слово "empty"  и будем анализировать название соответствующего файла:

var sImageSource = srcElement.src;

if (sImageSource.indexOf("empty") == -1)

{
  ...

Потом мы проверим текущий статус папки. Если она свернута, то нам потребуется ее развернуть, и наоборот. Статус будем определять исходя из значения параметра style.display. Если его значение равно "none", это означает, что пункт скрыт и свернут. А пустое значение будет означать, что он видим и развернут:

if (targetElement.style.display == "none")

{
  targetElement.style.display = "";
 


  if (srcElement.className == "LEVEL1")
    srcElement.src = "images/minusonly.gif";
  else
    srcElement.src = "images/folderopen.gif";

}

else


{
  targetElement.style.display = "none";
  if (srcElement.className == "LEVEL1")
    srcElement.src = "images/plusonly.gif";

 else
    srcElement.src = "images/folderclosed.gif";

}

И наконец, функция, помогающая обнаружить ошибку и устранить ее:

Sub sbShowXMLParseError(byVal objDocument)
               dim objParseError
               Set objParseError = objDocument.parseError
               Response.Write "XML File failed to load<BR>"
               Response.Write "---------------------------<BR>"
               Response.Write "Error: " & objParseError.reason & "<BR>"
  Response.Write "Line: " & objParseError.Line & "<BR>"
  Response.Write "Line Position: " & objParseError.linepos & "<BR>"
  Response.Write "Position In File: " & objParseError.filepos & "<BR>"
  Response.Write "Source Text: " & objParseError.srcText & "<BR>"
  Response.Write "Document URL: " & objParseError.url & "<BR>"
  set objParseError = nothing

 end sub
В начало В начало

Правый фрейм — файл Content.asp

Файл, по сути, содержит интерпретатор передаваемого ему параметра Page:

<%@ Language=VBScript %>
 <HTML>
 <HEAD></HEAD>
 <BODY>
 <%
 Dim sPage
 sPage = Request.QueryString("page")
 
 select case (sPage)
             case "":
             %>Please choose a menu item on the left<%
             case "usa":
             %>United States of America<%
             case "states":
             %>States<%
             case "ca":
             %>California<%
             case "nj":
             %>New Jersey<%
             case "az":
             %>Arizona<%
             case "histfig":
             %>Historical Figures<%
             case "george":
             %>George Washington<%
             case "tom":
             %>Thomas Jefferson<%
             case "history":
             %>History<%
             case "20th":
             %>20th Century<%
             case "inv":
             %>Inventions<%
             case "tec":
             %>Technology<%
             case "radio":
             %>Radio<%
             case "invprof":
             %>Inventor Profile<%
             case "first":
             %>First Uses<%
             case "computers":
             %>Computers<%
             case "begin":
             %>Beginnings<%
             case "sum":
             %>Summary<%
             case "trans":
             %>Transistor<%
             case "inventor":
             %>Inventor<%
             case "app":
             %>Applications<%
             case "wars":
             %>Wars<%
             case "wwi":
             %>World War I<%
             case "wwii":
             %>World War II<%
             case "viet":
             %>Vietnam<%
             case "21st":
             %>21st Century<%
             case else:
             %>Your menu selection is not recognized.<%
 end select
 %>
 </BODY>
 </HTML> 
В начало В начало

Заключение

Система динамического иерархического меню является достаточно мощным и удобным инструментом, позволяющим пользователю получить доступ к нужным разделам любой иерархии объектов, независимо от их характера и структуры. Данная система может быть с успехом применена в иерархиях практически любой степени сложности, особенно при организации сложных электронных магазинов, в которых имеется большое количество уровней вложенности категорий товаров. Реализация системы являет собой удачное сочетание использования технологий Web-программирования — ASP, XML и JavaScript, каждая из которых используется с определенной целью, а именно:

Архив исходных текстов к настоящей статье лежит здесь.

В статье использованы материалы ресурса: http://www.4guysfromrolla.com

КомпьютерПресс 1'2002